In [ ]:


In [ ]:


In [ ]:


In [ ]:

树的一家


In [ ]:


In [ ]:


In [ ]:

二叉树

基本术语

  • 最顶层的节点被称为根(root)节点, 即没有任何父节点的节点
  • 没有任何子节点的节点被称为叶节点(leaf node)或者终端节点(terminal node)
  • 树的高度(Height)是最深的叶节点与根节点之间的距离(即边的数量) 例如, A 的度是 3, I 的度是 0
  • 深度(Depth)或者层次(level)是节点与根节点的距离 例如, H 的深度是 2, B 的深度是 1

如果将树视作退化的 "图", 树的高度也可以视作 "度", 即边的数量

当一棵树的所有节点最多只有两个子节点时,被称为二叉树

应用场景

  • Maps
  • Sets
  • 数据库
  • 优先队列
  • 在 LDAP(Lightweight Directory Access Protocol)中查找相应信息
  • 在 XML/HTML 文件中,使用文档对象模型(DOM)接口进行搜索

特殊属性

  • 满二叉树: 任意一个节点只有零或两个子节点的树
  • 完全二叉树: 若二叉树的深度为h,除第h层外,其它各层的节点数都达到最大个数,第h层所有节点都连续集中在最左边
  • 完美二叉树: 满足完全二叉树性质,树的叶子节点均在最后一层的二叉树
  • 平衡二叉树: 当树有N个节点时, 高度不超过O(log N), 例如 AVL 树,红黑树

前三个属性有什么用? 基本都跟树的平衡性有关, 后面可以看到, 树在 "均衡" 生长时查找效率较高

满二叉树的样例

          18             
        /     \  
      15        30  
     /  \      /  \
   40    50  100   40

          18
        /    \   
      15     20    
     /  \       
   40    50   
  /  \
 30  50

      18
     /  \  
   40    30  
        /  \
      100   40


完全二叉树的样例

          18
       /      \  
     15        30  
    /  \       /  \
  40    50   100   40

          18
       /      \  
     15        30  
    /  \      /  \
  40    50  100   40
 /  \   /
8    7 9


树的性能

所有的树都需要考虑三个基本操作 查找节点, 插入节点, 删除节点 的性能, 每种操作都包括 平均性能最差情况的性能

在选择使用何种树时, 需要结合特定的使用场景, 如果不能得知应用场景, 其重要性排序可能是 查找节点 > 插入节点, 而 删除节点 的性能似乎比较少提到


In [ ]:


In [ ]:


In [ ]:


In [ ]:

实现基本的树

这里主要关心两点:

  • Node 的基本统计功能, 高度, 子孙数量等
  • Tree 的预览 show(), 其余功能以后慢慢加

代码可以用于了解树的基本概念和操作, 几乎肯定有 bug, 而且没有考虑性能...

预览功能 show() 用了图表库 Graphviz, 需要首先安装 pip install graphviz, Digraph 类有 _repr_svg_() 方法, 可以直接显示在 notebook 中, 参考 python-graphviz图数据可视化入门


In [1]:
from graphviz import Digraph
import uuid
from random import sample


class Node:
  
  def __init__(self, key):
    self.key = key
    self.left = None
    self.right = None
    self.parent = None
    self.is_root = False
    
  @property
  def depth(self):  
    # 该节点相对于根的深度, 必须为节点指定 parent, 只有根节点时算1
    return 1 if self.is_root else 1 + self.parent.depth
  
  @property
  def height(self):  
    # 该节点相对于自己子孙中最下一层的高度, 只有节点自身时算1
    height1 = self.left.height if self.left else 0
    height2 = self.right.height if self.right else 0
    return max(height1, height2) + 1

  @property
  def count(self):
    # 节点的所有子孙总数
    if self.left is None and self.right is None:
      return 1
    else:
      return self.left.count + self.right.count


  def __str__(self):
    return str(self.key)
  
  def __repr__(self):
    return "<{!r}> parent: {}, left: {}, right: {}".format(self.key, self.parent, self.left, self.right)

  

  
class Tree:
  
  def __init__(self):
    self.root = None

  def show(self, label=False, debug=False, size=None, node_surffix=lambda n: ''):
    if size is None:
      size = '{},{}'.format(self.root.height+1, self.root.height+1)
    self.dot = Digraph(comment='Binary Tree', graph_attr={'size': size})
    colors = ['aquamarine', 'khaki', 'skyblue', 'lightblue', 'orange', 'yellow', 'pink', 'lime', 'lightgreen']
    alpha = '22' if debug else '05'
    
    def pad_node(node_tag, child): 
      # 绘制跟child最大孩子数量的占位符, 
      # 否则自动排版后两个分支节点的位置会乱, 不好区分左右子树
      if child is None:
        return
      for i in range(2**(child.height-1)-1):
        pad_tag = str(uuid.uuid1())
        self.dot.node(pad_tag, "", color="#FF3366"+alpha) 
        self.dot.edge(node_tag, pad_tag, color="#999999"+alpha)

    def print_node(node, node_tag):  # 绘制以某个节点为根节点的二叉树
      if node.left is None and node.right is None:
        return
      left_tag = str(uuid.uuid1())  # 左节点的 uuid, 递归调用时用做连线 
      if node.left:
        pad_node(node_tag, node.left)
        self.dot.node(left_tag, str(node.left.key)+node_surffix(node.left), style='filled', color=color)  
        self.dot.edge(node_tag, left_tag, label=('L' if label else ''), color="#999999")   # 左节点与其父节点的连线
        pad_node(node_tag, node.left)
        print_node(node.left, left_tag)   # 递归调用, 打印下一层
      else:
        self.dot.node(left_tag, "", color="#FF3366"+alpha) # 占位的透明节点
        self.dot.edge(node_tag, left_tag, label=('L' if label else ''), color="#999999"+alpha)


      right_tag = str(uuid.uuid1())  
      if node.right:
        pad_node(node_tag, node.right)
        self.dot.node(right_tag, str(node.right.key)+node_surffix(node.right), style='filled', color=color)  
        self.dot.edge(node_tag, right_tag, label=('R' if label else ''), color="#999999")   # 左节点与其父节点的连线
        pad_node(node_tag, node.right)
        
        print_node(node.right, right_tag)   # 递归调用, 打印下一层
      else:
        self.dot.node(right_tag, "", color="#FF3366"+alpha) # 占位的透明节点
        self.dot.edge(node_tag, right_tag, label=('R' if label else ''), color="#999999"+alpha)
        
        
    if self.root is not None: 
      root_tag = str(uuid.uuid1())  # 根节点标签
      color = sample(colors,1)[0]    # 随机一个节点色
      self.dot.node(root_tag, str(self.root.key)+node_surffix(self.root), style='filled', color=color)  # 创建根节点
      print_node(self.root, root_tag)
    return self.dot

In [2]:
# 使用 Tree

tree = Tree()
tree.root = Node('A')
tree.root.left = Node('B')
tree.root.right = Node('C')
tree.root.left.right = Node('D')
tree.root.right.left = Node('E')
tree.root.right.right = Node('F')

tree.show(debug=False)


Out[2]:
%3 1faeb2c0-64e2-11e9-b398-7c67a27fbf6f A 1faed9d0-64e2-11e9-b222-7c67a27fbf6f 1faeb2c0-64e2-11e9-b398-7c67a27fbf6f->1faed9d0-64e2-11e9-b222-7c67a27fbf6f 1faeb2c1-64e2-11e9-9e7f-7c67a27fbf6f B 1faeb2c0-64e2-11e9-b398-7c67a27fbf6f->1faeb2c1-64e2-11e9-9e7f-7c67a27fbf6f 1faed9d1-64e2-11e9-a1f0-7c67a27fbf6f 1faeb2c0-64e2-11e9-b398-7c67a27fbf6f->1faed9d1-64e2-11e9-a1f0-7c67a27fbf6f 1faed9d5-64e2-11e9-b7a5-7c67a27fbf6f 1faeb2c0-64e2-11e9-b398-7c67a27fbf6f->1faed9d5-64e2-11e9-b7a5-7c67a27fbf6f 1faed9d4-64e2-11e9-b70f-7c67a27fbf6f C 1faeb2c0-64e2-11e9-b398-7c67a27fbf6f->1faed9d4-64e2-11e9-b70f-7c67a27fbf6f 1faed9d6-64e2-11e9-9a59-7c67a27fbf6f 1faeb2c0-64e2-11e9-b398-7c67a27fbf6f->1faed9d6-64e2-11e9-9a59-7c67a27fbf6f 1faed9d2-64e2-11e9-809e-7c67a27fbf6f 1faeb2c1-64e2-11e9-9e7f-7c67a27fbf6f->1faed9d2-64e2-11e9-809e-7c67a27fbf6f 1faed9d3-64e2-11e9-a251-7c67a27fbf6f D 1faeb2c1-64e2-11e9-9e7f-7c67a27fbf6f->1faed9d3-64e2-11e9-a251-7c67a27fbf6f 1faed9d7-64e2-11e9-92cf-7c67a27fbf6f E 1faed9d4-64e2-11e9-b70f-7c67a27fbf6f->1faed9d7-64e2-11e9-92cf-7c67a27fbf6f 1faed9d8-64e2-11e9-bbee-7c67a27fbf6f F 1faed9d4-64e2-11e9-b70f-7c67a27fbf6f->1faed9d8-64e2-11e9-bbee-7c67a27fbf6f

In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:

二叉搜索树, BST

也叫有序二叉树 / Binary Search Tree / BST

BST 节点的所有左孩子都比节点自身小或相等, 右孩子都比节点自身大

        10
      /    \
     6      30
    /      /  \
   4      15  40
  / \ 
 3   5


注: BST 通常不允许重复值的节点被添加到树中, 这时因为插入的是 "键", 类似 ID, 同样的键代表同一份数据, 如允许重复值, 重复节点应该作为右子节点, 有些 BST 的实现会记录重复的情况

二叉搜索树的查找和插入操作都很快, 二叉搜索树查找一个节点平均时间复杂度为 O(log N), 因此该数据结构适用于需要多次插入/查找数据的场景, 特殊情况时查找会慢很多

缺点

插入操作没有尝试平衡树, 最坏的情况下, 比如升序插入数据之后, 树可能只向某一个方向生长, 使之退化为链表, 查找复杂度变为 O(n), 需要改进树结构来解决问题

从左到右依次是最好, 典型, 最坏情况

实现基本的 BST


In [3]:
class BinarySearchTree(Tree):
  def __init__(self):
    super().__init__()
    
  def insert(self, key):
    new_node = Node(key)
    if self.root is None:
      self.root = new_node
      return

    curr_node = self.root
    while curr_node is not None:
      parent_node = curr_node
      if new_node.key == curr_node.key:
        print('detect duplicate node "{}"! '.format(new_node))
        return
      elif new_node.key < curr_node.key:
        curr_node = curr_node.left
      else:
        curr_node = curr_node.right
    if new_node.key < parent_node.key:
      parent_node.left = new_node
    else:
      parent_node.right = new_node
    new_node.parent = parent_node
    return new_node
  
  def lookup(self, key):
    if self.root is None: return None
    curr_node = self.root
    while curr_node is not None:
      if curr_node.key == key: 
        return curr_node
      elif key < curr_node.key:
        curr_node = curr_node.left
      else:
        curr_node = curr_node.right
    return None

In [4]:
tree = BinarySearchTree()
tree.root = Node('C')
tree.insert('B')
tree.insert('A')
tree.insert('D')
tree.insert('F')
tree.insert('E')
tree.insert('A')  # 重复, 不能插入

tree.show(debug=False)


detect duplicate node "A"! 
Out[4]:
%3 20029f22-64e2-11e9-ac68-7c67a27fbf6f C 20029f24-64e2-11e9-af15-7c67a27fbf6f 20029f22-64e2-11e9-ac68-7c67a27fbf6f->20029f24-64e2-11e9-af15-7c67a27fbf6f 20029f23-64e2-11e9-9e6c-7c67a27fbf6f B 20029f22-64e2-11e9-ac68-7c67a27fbf6f->20029f23-64e2-11e9-9e6c-7c67a27fbf6f 20029f25-64e2-11e9-a601-7c67a27fbf6f 20029f22-64e2-11e9-ac68-7c67a27fbf6f->20029f25-64e2-11e9-a601-7c67a27fbf6f 20029f29-64e2-11e9-9189-7c67a27fbf6f 20029f22-64e2-11e9-ac68-7c67a27fbf6f->20029f29-64e2-11e9-9189-7c67a27fbf6f 20029f2a-64e2-11e9-bbea-7c67a27fbf6f 20029f22-64e2-11e9-ac68-7c67a27fbf6f->20029f2a-64e2-11e9-bbea-7c67a27fbf6f 20029f2b-64e2-11e9-b705-7c67a27fbf6f 20029f22-64e2-11e9-ac68-7c67a27fbf6f->20029f2b-64e2-11e9-b705-7c67a27fbf6f 20029f28-64e2-11e9-8033-7c67a27fbf6f D 20029f22-64e2-11e9-ac68-7c67a27fbf6f->20029f28-64e2-11e9-8033-7c67a27fbf6f 20029f2c-64e2-11e9-a384-7c67a27fbf6f 20029f22-64e2-11e9-ac68-7c67a27fbf6f->20029f2c-64e2-11e9-a384-7c67a27fbf6f 20029f2d-64e2-11e9-949a-7c67a27fbf6f 20029f22-64e2-11e9-ac68-7c67a27fbf6f->20029f2d-64e2-11e9-949a-7c67a27fbf6f 2002c630-64e2-11e9-8b01-7c67a27fbf6f 20029f22-64e2-11e9-ac68-7c67a27fbf6f->2002c630-64e2-11e9-8b01-7c67a27fbf6f 20029f26-64e2-11e9-b6af-7c67a27fbf6f A 20029f23-64e2-11e9-9e6c-7c67a27fbf6f->20029f26-64e2-11e9-b6af-7c67a27fbf6f 20029f27-64e2-11e9-b765-7c67a27fbf6f 20029f23-64e2-11e9-9e6c-7c67a27fbf6f->20029f27-64e2-11e9-b765-7c67a27fbf6f 2002c631-64e2-11e9-bf6d-7c67a27fbf6f 20029f28-64e2-11e9-8033-7c67a27fbf6f->2002c631-64e2-11e9-bf6d-7c67a27fbf6f 2002c633-64e2-11e9-8384-7c67a27fbf6f 20029f28-64e2-11e9-8033-7c67a27fbf6f->2002c633-64e2-11e9-8384-7c67a27fbf6f 2002c632-64e2-11e9-88c0-7c67a27fbf6f F 20029f28-64e2-11e9-8033-7c67a27fbf6f->2002c632-64e2-11e9-88c0-7c67a27fbf6f 2002c634-64e2-11e9-80af-7c67a27fbf6f 20029f28-64e2-11e9-8033-7c67a27fbf6f->2002c634-64e2-11e9-80af-7c67a27fbf6f 2002c635-64e2-11e9-afd9-7c67a27fbf6f E 2002c632-64e2-11e9-88c0-7c67a27fbf6f->2002c635-64e2-11e9-afd9-7c67a27fbf6f 2002c636-64e2-11e9-9728-7c67a27fbf6f 2002c632-64e2-11e9-88c0-7c67a27fbf6f->2002c636-64e2-11e9-9728-7c67a27fbf6f

In [5]:
print(tree.lookup('C'))
print(tree.lookup('A'))
print(tree.lookup('F'))
print(tree.lookup('X'))


C
A
F
None

In [ ]:

树的遍历

中序遍历(In-Order Traversal)

中序遍历访问节点的顺序是: 左子节点、节点本身、右子节点

以这棵树作为例子:

         10
       /    \
      5      30
    /       /  \
   4       15   40
 /
3

中序遍历将按照以下顺序输出对应的值:

3、4、5、10、15、30、40

也就是说, 如果待遍历的树是一颗二叉搜索树, 那输出值将是升序的

后序遍历(Post-Order Traversal)

后序遍历访问节点的顺序是: 左子节点、右子节点、节点本身

后序遍历将按照以下顺序输出对应的值:

3、4、5、15、40、30、10


先序遍历(Pre-Order Traversal)与 深度优先搜索 (DFS)

先序遍历访问节点的顺序是: 节点本身、左子节点、右子节点

先序遍历将按照以下顺序输出对应的值:

10、5、4、3、30、15、40

先序遍历与深度优先搜索(DPS)的顺序一致

广度优先搜索 (BFS)

BFS 将按照以下顺序输出对应的值:

10、5、30、4、15、40、3

实现基本的遍历


In [6]:
class TraversalBinarySearchTree(BinarySearchTree):
  def __init__(self):
    super().__init__()
    
  def traversal(self, method='in_order'):
    if self.root is None: return []
    
    if method == 'in_order': return list(self.in_order(self.root))
    if method == 'pre_order': return list(self.pre_order(self.root))
    if method == 'post_order': return list(self.post_order(self.root))
    if method == 'dfs': return list(self.depth_first(self.root))
    if method == 'bfs': return list(self.breadth_first(self.root))
  
  
  def in_order(self, node):
    if not node: 
      return
    for n in self.in_order(node.left): yield n
    yield node
    for n in self.in_order(node.right): yield n

  def pre_order(self, node):
    if not node: 
      return
    yield node
    for n in self.pre_order(node.left): yield n
    for n in self.pre_order(node.right): yield n

  def post_order(self, node):
    if not node: 
      return
    for n in self.post_order(node.left): yield n
    for n in self.post_order(node.right): yield n
    yield node
      
  def depth_first(self, node):
    # 跟先序一样
    if not node: 
      return
    yield node
    for n in self.depth_first(node.left): yield n
    for n in self.depth_first(node.right): yield n
      
  def breadth_first(self, node):
    # 这个不会用递归, 只会用队列写
    queue = [node]
    while queue:
      current = queue.pop(0)
      if current.left: queue.append(current.left)
      if current.right: queue.append(current.right)
      yield current

In [7]:
# 构造以下这棵树, 演示遍历:
#          10
#        /    \
#       5      30
#     /       /  \
#    4       15   40
#  /
# 3
tree = TraversalBinarySearchTree()
tree.root = Node(10)
tree.insert(5)
tree.insert(30)
tree.insert(4)
tree.insert(3)
tree.insert(15)
tree.insert(40)
tree.show()


Out[7]:
%3 203b6262-64e2-11e9-8bbb-7c67a27fbf6f 10 203b8971-64e2-11e9-b036-7c67a27fbf6f 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b8971-64e2-11e9-b036-7c67a27fbf6f 203b8972-64e2-11e9-953d-7c67a27fbf6f 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b8972-64e2-11e9-953d-7c67a27fbf6f 203b8973-64e2-11e9-a874-7c67a27fbf6f 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b8973-64e2-11e9-a874-7c67a27fbf6f 203b8970-64e2-11e9-a04e-7c67a27fbf6f 5 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b8970-64e2-11e9-a04e-7c67a27fbf6f 203b8974-64e2-11e9-80b0-7c67a27fbf6f 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b8974-64e2-11e9-80b0-7c67a27fbf6f 203b8975-64e2-11e9-8c23-7c67a27fbf6f 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b8975-64e2-11e9-8c23-7c67a27fbf6f 203b8976-64e2-11e9-81af-7c67a27fbf6f 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b8976-64e2-11e9-81af-7c67a27fbf6f 203b897e-64e2-11e9-af6b-7c67a27fbf6f 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b897e-64e2-11e9-af6b-7c67a27fbf6f 203b897d-64e2-11e9-8a56-7c67a27fbf6f 30 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b897d-64e2-11e9-8a56-7c67a27fbf6f 203b897f-64e2-11e9-accd-7c67a27fbf6f 203b6262-64e2-11e9-8bbb-7c67a27fbf6f->203b897f-64e2-11e9-accd-7c67a27fbf6f 203b8978-64e2-11e9-904f-7c67a27fbf6f 203b8970-64e2-11e9-a04e-7c67a27fbf6f->203b8978-64e2-11e9-904f-7c67a27fbf6f 203b8977-64e2-11e9-bcfe-7c67a27fbf6f 4 203b8970-64e2-11e9-a04e-7c67a27fbf6f->203b8977-64e2-11e9-bcfe-7c67a27fbf6f 203b8979-64e2-11e9-a7ce-7c67a27fbf6f 203b8970-64e2-11e9-a04e-7c67a27fbf6f->203b8979-64e2-11e9-a7ce-7c67a27fbf6f 203b897c-64e2-11e9-8e56-7c67a27fbf6f 203b8970-64e2-11e9-a04e-7c67a27fbf6f->203b897c-64e2-11e9-8e56-7c67a27fbf6f 203b897a-64e2-11e9-9ced-7c67a27fbf6f 3 203b8977-64e2-11e9-bcfe-7c67a27fbf6f->203b897a-64e2-11e9-9ced-7c67a27fbf6f 203b897b-64e2-11e9-98b9-7c67a27fbf6f 203b8977-64e2-11e9-bcfe-7c67a27fbf6f->203b897b-64e2-11e9-98b9-7c67a27fbf6f 203bb080-64e2-11e9-819a-7c67a27fbf6f 15 203b897d-64e2-11e9-8a56-7c67a27fbf6f->203bb080-64e2-11e9-819a-7c67a27fbf6f 203bb081-64e2-11e9-8127-7c67a27fbf6f 40 203b897d-64e2-11e9-8a56-7c67a27fbf6f->203bb081-64e2-11e9-8127-7c67a27fbf6f

In [8]:
print("中序:          ", [str(elem) for elem in tree.traversal('in_order')])
print("先序:          ", [str(elem) for elem in tree.traversal('pre_order')])
print("后序:          ", [str(elem) for elem in tree.traversal('post_order')])
print("深度优先 (先序):", [str(elem) for elem in tree.traversal('dfs')])
print("广度优先:       ", [str(elem) for elem in tree.traversal('bfs')])


中序:           ['3', '4', '5', '10', '15', '30', '40']
先序:           ['10', '5', '4', '3', '30', '15', '40']
后序:           ['3', '4', '5', '15', '40', '30', '10']
深度优先 (先序): ['10', '5', '4', '3', '30', '15', '40']
广度优先:        ['10', '5', '30', '4', '15', '40', '3']

In [ ]:


In [ ]:

AVL 树

较早期出现的一种自平衡的二叉搜索树, 得名于其发明者 (Adelson-Velskii 和 Landis)

递归定义:

  • 左右子树的高度差小于等于 1
  • 其每一个子树均为 AVL 树

为了保证二叉树的平衡, AVL 树引入了监督机制, 在树的某部分的不平衡度超过阈值后触发平衡操作, 保证平衡度在可以接受的范围内

监督指标被称为 平衡因子(Balance Factor): 某个节点的左子树的高度减去右子树的高度差

维护动态平衡

动态平衡: 在对树进行插入操作时, 每个状态都能满足平衡条件

动态平衡是时时刻刻的, 在新数据插入前树是平衡的, 一旦数据插入导致树结构不平衡时, 则马上进行调整, 如果不去时刻维护, 以后再要获得全局信息代价高昂, 且全局调整难度大于局部调整

再平衡操作

二叉树的平衡化有两大基础操作: 左旋和右旋, 左旋即逆时针旋转, 右旋即顺时针旋转, 这两种操作是对称的

这种旋转在整个平衡化过程中可能进行一次或多次, 这两种操作都是从失去平衡的最小子树根节点开始的 (即离插入节点最近且平衡因子超过1的祖节点)

以右旋为例, 右旋操作就是把上图中的 B 节点和 E 节点进行“父子交换”, 在仅有这三个节点时很简单

    E      
   /
  B        B
 /        / \
A        A   E

但是, 当B节点处存在右孩子时, 稍有点复杂, 通常操作是抛弃右孩子, 将其与旋转后的节点E相连, 成为节点E的左孩子

    E      
   /
  B         B
 / \       / \
A   C     A   E
             /
            C

需要平衡的四种情况

LL 型

示意一次右旋操作

LL 型就是上图左边那种情况, 因为在根节点的左孩子的左子树添加了新节点, 导致根节点的平衡因子变为+2, 二叉树失去平衡, 这种情况对节点n右旋一次即可

RR 型

RR 型的情况和 LL 型完全对称, 只需对节点n进行一次左旋即可修正

LR 型

示意一次左旋, 接一次右旋操作

LR 是将新的节点插入到了n的左孩子的右子树上导致的不平衡的情况, 需要先对 i 进行一次左旋, 再对 n 进行一次右旋

RL 型

RL 是将新的节点插入到了 n 的右孩子的左子树上导致的不平衡的情况, 需要先对 i 进行一次右旋, 再对 n 进行一次左旋

这四种情况的判断很简单, 根据破坏树的平衡性(平衡因子的绝对值大于 1)的节点以及其子节点的平衡因子来判断平衡化类型

可得出如下表格:

问题节点 左孩子 右孩子 类型
+2 +1 - LL
+2 -1 - LR
-2 - +1 RL
-2 - -1 RR

AVL 插入新节点所需要的最大旋转次数是常数, 不需要一直旋转到根节点, 这里参考 AVL 树的谣言

根据 AVL 的定义, 可以做出两个推论, 再平衡向上回溯时:

  • 对于插入更新, 如当前节点的高度没有改变, 则上面所有父节点的高度和平衡也不会改变
  • 对于删除更新, 如当前节点的高度没有改变且平衡值在 [-1, 1] 区间, 则所有父节点的高度和平衡都不会改变

根据这两个推论, AVL 的插入和删除大部分时候只需要向上回溯一两个节点即可, 范围十分紧凑

这个说法对吗? 为什么?


In [ ]:

实现基本的 AVL 树


In [9]:
class AVLTree(TraversalBinarySearchTree):
  def __init__(self):
    super().__init__()

  def get_balance_factor(self, node):
    h1 = node.left.height if node.left else 0
    h2 = node.right.height if node.right else 0
    return h1 - h2

  def bond(self, parent_node, node):
    if parent_node.key < node.key:
      parent_node.right = node
    else:
      parent_node.left = node
    node.parent = parent_node
  
  def left_rotate(self, node):
    father = node.parent  # node 如果有上游, 最后也需要修复链接关系
    son = node.right                     #   A                C  
    grandson = son.left                  #  / \              / \   
    son.left = node                      # B   C            A   Cr  
    node.parent = son                    #    / \    -->   / \   \  
    node.right = grandson                #   Cl  Cr       B  Cl  UC  
    if grandson: grandson.parent = node  #        \
    if father:                           #        UC
      self.bond(father, son)
    else:  # father 没有 parent, 所以  father 是 根节点
      self.root = son
      son.parent = None

  def right_rotate(self, node):
    father = node.parent  # node 如果有上游, 最后也需要修复链接关系 
    son = node.left                      #        A               B  
    grandson = son.right                 #       / \             / \   
    son.right = node                     #      B   C           Bl  A  
    node.parent = son                    #     / \      -->    /   / \  
    node.left = grandson                 #    Bl  Br          UB Br  C  
    if grandson: grandson.parent = node  #   /       
    if father:                           #  UB 
      self.bond(father, son)
    else:  # node 没有 parent, 所以 node 是 根节点
      self.root = son
      son.parent = None

In [10]:
tree = AVLTree()  # 构建 BST, 这个实现中, 插入节点时没考虑平衡
tree.insert('n')
tree.insert('i')
tree.insert('p')
tree.insert('g')
tree.insert('k')
tree.insert('f')
tree.insert('h')
node_surffix = lambda n: " ({:+1d})".format(tree.get_balance_factor(n))
tree.show(node_surffix=node_surffix)


Out[10]:
%3 2068db00-64e2-11e9-a2a2-7c67a27fbf6f n (+2) 2068db02-64e2-11e9-9a27-7c67a27fbf6f 2068db00-64e2-11e9-a2a2-7c67a27fbf6f->2068db02-64e2-11e9-9a27-7c67a27fbf6f 2068db03-64e2-11e9-b439-7c67a27fbf6f 2068db00-64e2-11e9-a2a2-7c67a27fbf6f->2068db03-64e2-11e9-b439-7c67a27fbf6f 2068db04-64e2-11e9-b161-7c67a27fbf6f 2068db00-64e2-11e9-a2a2-7c67a27fbf6f->2068db04-64e2-11e9-b161-7c67a27fbf6f 2068db01-64e2-11e9-ae89-7c67a27fbf6f i (+1) 2068db00-64e2-11e9-a2a2-7c67a27fbf6f->2068db01-64e2-11e9-ae89-7c67a27fbf6f 2068db05-64e2-11e9-9860-7c67a27fbf6f 2068db00-64e2-11e9-a2a2-7c67a27fbf6f->2068db05-64e2-11e9-9860-7c67a27fbf6f 2068db06-64e2-11e9-a5a1-7c67a27fbf6f 2068db00-64e2-11e9-a2a2-7c67a27fbf6f->2068db06-64e2-11e9-a5a1-7c67a27fbf6f 2068db07-64e2-11e9-9454-7c67a27fbf6f 2068db00-64e2-11e9-a2a2-7c67a27fbf6f->2068db07-64e2-11e9-9454-7c67a27fbf6f 20690214-64e2-11e9-a687-7c67a27fbf6f p (+0) 2068db00-64e2-11e9-a2a2-7c67a27fbf6f->20690214-64e2-11e9-a687-7c67a27fbf6f 2068db09-64e2-11e9-acb7-7c67a27fbf6f 2068db01-64e2-11e9-ae89-7c67a27fbf6f->2068db09-64e2-11e9-acb7-7c67a27fbf6f 2068db08-64e2-11e9-80d7-7c67a27fbf6f g (+0) 2068db01-64e2-11e9-ae89-7c67a27fbf6f->2068db08-64e2-11e9-80d7-7c67a27fbf6f 20690210-64e2-11e9-915b-7c67a27fbf6f 2068db01-64e2-11e9-ae89-7c67a27fbf6f->20690210-64e2-11e9-915b-7c67a27fbf6f 20690213-64e2-11e9-a9f5-7c67a27fbf6f k (+0) 2068db01-64e2-11e9-ae89-7c67a27fbf6f->20690213-64e2-11e9-a9f5-7c67a27fbf6f 20690211-64e2-11e9-978c-7c67a27fbf6f f (+0) 2068db08-64e2-11e9-80d7-7c67a27fbf6f->20690211-64e2-11e9-978c-7c67a27fbf6f 20690212-64e2-11e9-8c88-7c67a27fbf6f h (+0) 2068db08-64e2-11e9-80d7-7c67a27fbf6f->20690212-64e2-11e9-8c88-7c67a27fbf6f

In [11]:
n = tree.lookup('n')
print(repr(n))
tree.right_rotate(n)  # 右旋 node_n, 这之后更平衡一些
tree.show(node_surffix=node_surffix)


<'n'> parent: None, left: i, right: p
Out[11]:
%3 207dc290-64e2-11e9-b2c8-7c67a27fbf6f i (+0) 207dc292-64e2-11e9-8910-7c67a27fbf6f 207dc290-64e2-11e9-b2c8-7c67a27fbf6f->207dc292-64e2-11e9-8910-7c67a27fbf6f 207dc291-64e2-11e9-8428-7c67a27fbf6f g (+0) 207dc290-64e2-11e9-b2c8-7c67a27fbf6f->207dc291-64e2-11e9-8428-7c67a27fbf6f 207dc293-64e2-11e9-8601-7c67a27fbf6f 207dc290-64e2-11e9-b2c8-7c67a27fbf6f->207dc293-64e2-11e9-8601-7c67a27fbf6f 207dc297-64e2-11e9-bcff-7c67a27fbf6f 207dc290-64e2-11e9-b2c8-7c67a27fbf6f->207dc297-64e2-11e9-bcff-7c67a27fbf6f 207dc296-64e2-11e9-a45a-7c67a27fbf6f n (+0) 207dc290-64e2-11e9-b2c8-7c67a27fbf6f->207dc296-64e2-11e9-a45a-7c67a27fbf6f 207dc298-64e2-11e9-82a0-7c67a27fbf6f 207dc290-64e2-11e9-b2c8-7c67a27fbf6f->207dc298-64e2-11e9-82a0-7c67a27fbf6f 207dc294-64e2-11e9-b3f3-7c67a27fbf6f f (+0) 207dc291-64e2-11e9-8428-7c67a27fbf6f->207dc294-64e2-11e9-b3f3-7c67a27fbf6f 207dc295-64e2-11e9-ba44-7c67a27fbf6f h (+0) 207dc291-64e2-11e9-8428-7c67a27fbf6f->207dc295-64e2-11e9-ba44-7c67a27fbf6f 207dc299-64e2-11e9-91a4-7c67a27fbf6f k (+0) 207dc296-64e2-11e9-a45a-7c67a27fbf6f->207dc299-64e2-11e9-91a4-7c67a27fbf6f 207dc29a-64e2-11e9-9417-7c67a27fbf6f p (+0) 207dc296-64e2-11e9-a45a-7c67a27fbf6f->207dc29a-64e2-11e9-9417-7c67a27fbf6f

In [12]:
i = tree.lookup('i')
print(repr(i))
tree.right_rotate(i)   # 右旋 node_i, 只是演示右旋操作, 如果为了平衡这里不该这么旋转
tree.show(node_surffix=node_surffix)  # 这时 node_g 变成了 "问题节点"


<'i'> parent: None, left: g, right: n
Out[12]:
%3 2094a5f0-64e2-11e9-b8f2-7c67a27fbf6f g (-2) 2094cd00-64e2-11e9-829c-7c67a27fbf6f f (+0) 2094a5f0-64e2-11e9-b8f2-7c67a27fbf6f->2094cd00-64e2-11e9-829c-7c67a27fbf6f 2094cd02-64e2-11e9-99e0-7c67a27fbf6f 2094a5f0-64e2-11e9-b8f2-7c67a27fbf6f->2094cd02-64e2-11e9-99e0-7c67a27fbf6f 2094cd03-64e2-11e9-8143-7c67a27fbf6f 2094a5f0-64e2-11e9-b8f2-7c67a27fbf6f->2094cd03-64e2-11e9-8143-7c67a27fbf6f 2094cd04-64e2-11e9-8cc0-7c67a27fbf6f 2094a5f0-64e2-11e9-b8f2-7c67a27fbf6f->2094cd04-64e2-11e9-8cc0-7c67a27fbf6f 2094cd01-64e2-11e9-a378-7c67a27fbf6f i (-1) 2094a5f0-64e2-11e9-b8f2-7c67a27fbf6f->2094cd01-64e2-11e9-a378-7c67a27fbf6f 2094cd05-64e2-11e9-83cb-7c67a27fbf6f 2094a5f0-64e2-11e9-b8f2-7c67a27fbf6f->2094cd05-64e2-11e9-83cb-7c67a27fbf6f 2094cd06-64e2-11e9-8a02-7c67a27fbf6f 2094a5f0-64e2-11e9-b8f2-7c67a27fbf6f->2094cd06-64e2-11e9-8a02-7c67a27fbf6f 2094cd07-64e2-11e9-856b-7c67a27fbf6f 2094a5f0-64e2-11e9-b8f2-7c67a27fbf6f->2094cd07-64e2-11e9-856b-7c67a27fbf6f 2094cd08-64e2-11e9-aa7a-7c67a27fbf6f h (+0) 2094cd01-64e2-11e9-a378-7c67a27fbf6f->2094cd08-64e2-11e9-aa7a-7c67a27fbf6f 2094cd0a-64e2-11e9-9ce4-7c67a27fbf6f 2094cd01-64e2-11e9-a378-7c67a27fbf6f->2094cd0a-64e2-11e9-9ce4-7c67a27fbf6f 2094cd09-64e2-11e9-b19b-7c67a27fbf6f n (+0) 2094cd01-64e2-11e9-a378-7c67a27fbf6f->2094cd09-64e2-11e9-b19b-7c67a27fbf6f 2094cd0b-64e2-11e9-86f4-7c67a27fbf6f 2094cd01-64e2-11e9-a378-7c67a27fbf6f->2094cd0b-64e2-11e9-86f4-7c67a27fbf6f 2094f410-64e2-11e9-83a3-7c67a27fbf6f k (+0) 2094cd09-64e2-11e9-b19b-7c67a27fbf6f->2094f410-64e2-11e9-83a3-7c67a27fbf6f 2094f411-64e2-11e9-8292-7c67a27fbf6f p (+0) 2094cd09-64e2-11e9-b19b-7c67a27fbf6f->2094f411-64e2-11e9-8292-7c67a27fbf6f

In [13]:
tree.left_rotate(tree.lookup('g'))    # 左旋 node_g
tree.show(node_surffix=node_surffix)


Out[13]:
%3 20b01d2e-64e2-11e9-be78-7c67a27fbf6f i (+0) 20b01d30-64e2-11e9-8c60-7c67a27fbf6f 20b01d2e-64e2-11e9-be78-7c67a27fbf6f->20b01d30-64e2-11e9-8c60-7c67a27fbf6f 20b01d2f-64e2-11e9-880b-7c67a27fbf6f g (+0) 20b01d2e-64e2-11e9-be78-7c67a27fbf6f->20b01d2f-64e2-11e9-880b-7c67a27fbf6f 20b01d31-64e2-11e9-8e5f-7c67a27fbf6f 20b01d2e-64e2-11e9-be78-7c67a27fbf6f->20b01d31-64e2-11e9-8e5f-7c67a27fbf6f 20b01d35-64e2-11e9-9745-7c67a27fbf6f 20b01d2e-64e2-11e9-be78-7c67a27fbf6f->20b01d35-64e2-11e9-9745-7c67a27fbf6f 20b01d34-64e2-11e9-af95-7c67a27fbf6f n (+0) 20b01d2e-64e2-11e9-be78-7c67a27fbf6f->20b01d34-64e2-11e9-af95-7c67a27fbf6f 20b01d36-64e2-11e9-a8fe-7c67a27fbf6f 20b01d2e-64e2-11e9-be78-7c67a27fbf6f->20b01d36-64e2-11e9-a8fe-7c67a27fbf6f 20b01d32-64e2-11e9-9406-7c67a27fbf6f f (+0) 20b01d2f-64e2-11e9-880b-7c67a27fbf6f->20b01d32-64e2-11e9-9406-7c67a27fbf6f 20b01d33-64e2-11e9-8d45-7c67a27fbf6f h (+0) 20b01d2f-64e2-11e9-880b-7c67a27fbf6f->20b01d33-64e2-11e9-8d45-7c67a27fbf6f 20b01d37-64e2-11e9-bd7a-7c67a27fbf6f k (+0) 20b01d34-64e2-11e9-af95-7c67a27fbf6f->20b01d37-64e2-11e9-bd7a-7c67a27fbf6f 20b01d38-64e2-11e9-a10d-7c67a27fbf6f p (+0) 20b01d34-64e2-11e9-af95-7c67a27fbf6f->20b01d38-64e2-11e9-a10d-7c67a27fbf6f

总结:

  • 左旋和右旋的操作对象 right_rotate(node), 这个 node 一般指 "问题节点"
  • 虽然由 "问题节点" 触发旋转, 但旋转的 "轴心" 看起来更像 "问题节点" 的某个孩子
  • "问题节点" 触发旋转时, 必然是 +2-2, 不会有更高的不平衡值
  • 旋转后 "轴心" 将换到最高的位置, 不平衡值一定是 0?
  • 如果 "问题节点" +2, 这时就不关心右孩子, 只考查它的左孩子是 +1 (执行 LL) 还是 -1 (执行 LR)

In [14]:
tree = AVLTree()  # 构建另一个 BST, 演示 LR 型旋转
tree.insert('n')
tree.insert('i')
tree.insert('p')
tree.insert('g')
tree.insert('k')
tree.insert('j')   # 插入这个节点后, 导致 node_n 变为 +2, 之后需要使用 LR 修正
node_surffix = lambda n: " ({:+1d})".format(tree.get_balance_factor(n))
tree.show(node_surffix=node_surffix)


Out[14]:
%3 20c79cd0-64e2-11e9-a42a-7c67a27fbf6f n (+2) 20c79cd2-64e2-11e9-8385-7c67a27fbf6f 20c79cd0-64e2-11e9-a42a-7c67a27fbf6f->20c79cd2-64e2-11e9-8385-7c67a27fbf6f 20c79cd3-64e2-11e9-8081-7c67a27fbf6f 20c79cd0-64e2-11e9-a42a-7c67a27fbf6f->20c79cd3-64e2-11e9-8081-7c67a27fbf6f 20c79cd4-64e2-11e9-b9ea-7c67a27fbf6f 20c79cd0-64e2-11e9-a42a-7c67a27fbf6f->20c79cd4-64e2-11e9-b9ea-7c67a27fbf6f 20c79cd1-64e2-11e9-9455-7c67a27fbf6f i (-1) 20c79cd0-64e2-11e9-a42a-7c67a27fbf6f->20c79cd1-64e2-11e9-9455-7c67a27fbf6f 20c79cd5-64e2-11e9-88e9-7c67a27fbf6f 20c79cd0-64e2-11e9-a42a-7c67a27fbf6f->20c79cd5-64e2-11e9-88e9-7c67a27fbf6f 20c79cd6-64e2-11e9-9796-7c67a27fbf6f 20c79cd0-64e2-11e9-a42a-7c67a27fbf6f->20c79cd6-64e2-11e9-9796-7c67a27fbf6f 20c7c3e2-64e2-11e9-ac76-7c67a27fbf6f 20c79cd0-64e2-11e9-a42a-7c67a27fbf6f->20c7c3e2-64e2-11e9-ac76-7c67a27fbf6f 20c7c3e9-64e2-11e9-b5bb-7c67a27fbf6f p (+0) 20c79cd0-64e2-11e9-a42a-7c67a27fbf6f->20c7c3e9-64e2-11e9-b5bb-7c67a27fbf6f 20c7c3e3-64e2-11e9-a49f-7c67a27fbf6f g (+0) 20c79cd1-64e2-11e9-9455-7c67a27fbf6f->20c7c3e3-64e2-11e9-a49f-7c67a27fbf6f 20c7c3e5-64e2-11e9-b8fb-7c67a27fbf6f 20c79cd1-64e2-11e9-9455-7c67a27fbf6f->20c7c3e5-64e2-11e9-b8fb-7c67a27fbf6f 20c7c3e4-64e2-11e9-8147-7c67a27fbf6f k (+1) 20c79cd1-64e2-11e9-9455-7c67a27fbf6f->20c7c3e4-64e2-11e9-8147-7c67a27fbf6f 20c7c3e6-64e2-11e9-b5ba-7c67a27fbf6f 20c79cd1-64e2-11e9-9455-7c67a27fbf6f->20c7c3e6-64e2-11e9-b5ba-7c67a27fbf6f 20c7c3e7-64e2-11e9-89e5-7c67a27fbf6f j (+0) 20c7c3e4-64e2-11e9-8147-7c67a27fbf6f->20c7c3e7-64e2-11e9-89e5-7c67a27fbf6f 20c7c3e8-64e2-11e9-bfc3-7c67a27fbf6f 20c7c3e4-64e2-11e9-8147-7c67a27fbf6f->20c7c3e8-64e2-11e9-bfc3-7c67a27fbf6f

In [15]:
tree.left_rotate(tree.lookup('i'))  # 首先左旋 node_i
tree.show(node_surffix=node_surffix)


Out[15]:
%3 20dea740-64e2-11e9-bae7-7c67a27fbf6f n (+2) 20dece51-64e2-11e9-bd47-7c67a27fbf6f 20dea740-64e2-11e9-bae7-7c67a27fbf6f->20dece51-64e2-11e9-bd47-7c67a27fbf6f 20dece52-64e2-11e9-8d67-7c67a27fbf6f 20dea740-64e2-11e9-bae7-7c67a27fbf6f->20dece52-64e2-11e9-8d67-7c67a27fbf6f 20dece53-64e2-11e9-9d23-7c67a27fbf6f 20dea740-64e2-11e9-bae7-7c67a27fbf6f->20dece53-64e2-11e9-9d23-7c67a27fbf6f 20dece50-64e2-11e9-b852-7c67a27fbf6f k (+2) 20dea740-64e2-11e9-bae7-7c67a27fbf6f->20dece50-64e2-11e9-b852-7c67a27fbf6f 20dece54-64e2-11e9-b9d5-7c67a27fbf6f 20dea740-64e2-11e9-bae7-7c67a27fbf6f->20dece54-64e2-11e9-b9d5-7c67a27fbf6f 20dece55-64e2-11e9-b971-7c67a27fbf6f 20dea740-64e2-11e9-bae7-7c67a27fbf6f->20dece55-64e2-11e9-b971-7c67a27fbf6f 20dece56-64e2-11e9-9180-7c67a27fbf6f 20dea740-64e2-11e9-bae7-7c67a27fbf6f->20dece56-64e2-11e9-9180-7c67a27fbf6f 20dece5d-64e2-11e9-b200-7c67a27fbf6f p (+0) 20dea740-64e2-11e9-bae7-7c67a27fbf6f->20dece5d-64e2-11e9-b200-7c67a27fbf6f 20dece58-64e2-11e9-8e50-7c67a27fbf6f 20dece50-64e2-11e9-b852-7c67a27fbf6f->20dece58-64e2-11e9-8e50-7c67a27fbf6f 20dece57-64e2-11e9-9ed0-7c67a27fbf6f i (+0) 20dece50-64e2-11e9-b852-7c67a27fbf6f->20dece57-64e2-11e9-9ed0-7c67a27fbf6f 20dece59-64e2-11e9-9304-7c67a27fbf6f 20dece50-64e2-11e9-b852-7c67a27fbf6f->20dece59-64e2-11e9-9304-7c67a27fbf6f 20dece5c-64e2-11e9-bc2e-7c67a27fbf6f 20dece50-64e2-11e9-b852-7c67a27fbf6f->20dece5c-64e2-11e9-bc2e-7c67a27fbf6f 20dece5a-64e2-11e9-a551-7c67a27fbf6f g (+0) 20dece57-64e2-11e9-9ed0-7c67a27fbf6f->20dece5a-64e2-11e9-a551-7c67a27fbf6f 20dece5b-64e2-11e9-bcd7-7c67a27fbf6f j (+0) 20dece57-64e2-11e9-9ed0-7c67a27fbf6f->20dece5b-64e2-11e9-bcd7-7c67a27fbf6f

In [16]:
tree.right_rotate(tree.lookup('n'))  # 然后右旋 node_n
tree.show(node_surffix=node_surffix)


Out[16]:
%3 20f4c750-64e2-11e9-8548-7c67a27fbf6f k (+0) 20f4c752-64e2-11e9-9bfe-7c67a27fbf6f 20f4c750-64e2-11e9-8548-7c67a27fbf6f->20f4c752-64e2-11e9-9bfe-7c67a27fbf6f 20f4c751-64e2-11e9-b9fe-7c67a27fbf6f i (+0) 20f4c750-64e2-11e9-8548-7c67a27fbf6f->20f4c751-64e2-11e9-b9fe-7c67a27fbf6f 20f4ee62-64e2-11e9-aaa4-7c67a27fbf6f 20f4c750-64e2-11e9-8548-7c67a27fbf6f->20f4ee62-64e2-11e9-aaa4-7c67a27fbf6f 20f4ee66-64e2-11e9-895e-7c67a27fbf6f 20f4c750-64e2-11e9-8548-7c67a27fbf6f->20f4ee66-64e2-11e9-895e-7c67a27fbf6f 20f4ee65-64e2-11e9-84f3-7c67a27fbf6f n (-1) 20f4c750-64e2-11e9-8548-7c67a27fbf6f->20f4ee65-64e2-11e9-84f3-7c67a27fbf6f 20f4ee67-64e2-11e9-af8e-7c67a27fbf6f 20f4c750-64e2-11e9-8548-7c67a27fbf6f->20f4ee67-64e2-11e9-af8e-7c67a27fbf6f 20f4ee63-64e2-11e9-bcd6-7c67a27fbf6f g (+0) 20f4c751-64e2-11e9-b9fe-7c67a27fbf6f->20f4ee63-64e2-11e9-bcd6-7c67a27fbf6f 20f4ee64-64e2-11e9-80fc-7c67a27fbf6f j (+0) 20f4c751-64e2-11e9-b9fe-7c67a27fbf6f->20f4ee64-64e2-11e9-80fc-7c67a27fbf6f 20f4ee68-64e2-11e9-89e4-7c67a27fbf6f 20f4ee65-64e2-11e9-84f3-7c67a27fbf6f->20f4ee68-64e2-11e9-89e4-7c67a27fbf6f 20f4ee69-64e2-11e9-9793-7c67a27fbf6f p (+0) 20f4ee65-64e2-11e9-84f3-7c67a27fbf6f->20f4ee69-64e2-11e9-9793-7c67a27fbf6f

有了基本的旋转操作, 接下来应该改进 insert, 使之在插入删除节点后, 自动修正平衡

大致流程应该是:

insert(node):
    找到该存放 node 的位置
    插入 node
    rebalance(node.parent)

rebalance(node):
    while 循环:
        计算node自身的平衡因子
        计算node孩子的平衡因子 (只关心其中一个孩子就行)
        区分应该使用哪种旋转 LL LR RL RR
        重算 node 的高度
        node = node.parent

In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:

多路查找树

也叫N叉查找树

典型二叉查找树结构的查找时间复杂度 O(log N) 与树深度相关,降低树的深度自然会提高查找效率

但是, 考虑大规模数据存储中实现索引查询的实际问题: 树节点存储的元素数量有限,这样导致二叉查找树结构由于树的深度过大而造成磁盘 I/O 读写过于频繁,进而导致查询效率低下

如何减少树的深度? 一个基本想法就是: 采用多叉树结构, 让每一个节点的孩子数可以多于两个, 且每一个节点处可以存储多个元素

当然由于它是查找树, 所有元素间还是需要存在排序关系

常见的几种形式:

  • 2-3树
  • 2-3-4树
  • B树
  • B+树

2-3查找树

允许节点保存一到两个键

  • 2-节点:含有一个键和两条链接,左链接都小于该节点,右链接都大于该节点
  • 3-节点:含有两个键和三条链接,左链接都小于该节点,中链接都位于该节点的两个键之间,右链接都大于该节点

设计缘由

BST 最大的问题在于对输入敏感, 在插入数据恰好是有序的情况下, BST 构建出来的结构相当于是链表

为什么会出现这种情况? 本质是当新节点加入时, 树没有选择 "节点去向" 的权力, 自然就没办法动态改变它的高度, 所以需要设计一种结构能够让树拥有分配权, 且可以动态调整自身结构

2-节点即传统的 BST 节点,而新引入了3-节点, 意味着树可以把新加入的节点 "暂存在这", 此后当满足一定条件时就可以在保证平衡的同时分裂, 这样就拥有了灵活的调整手段

操作的基本原则

查找: 跟普通二叉树道理相似, 但是多一个分支, 稍微复杂一点儿

插入:

  1. 不允许向末端插入, 否则会破坏平衡
  2. 当有新的元素插入时, 首先找到对应叶子节点, 如果有空余就直接存放 (生成一个3-节点)
  3. 新元素插入, 且叶子节点没有空余就分裂, 把分裂出来的新节点上浮到父节点中
  4. 上浮到父节点的过程可能会继续向上传导

在2-3树中, 只有唯一的情况会导致树高度增加: 即插入元素后, 分裂向上传导到根节点, 使根节点也分裂, 此时树高度+1, 按照这个原则, 2-3树永远是平衡的

3-节点的分裂

 A E      <---- 这时新加入 S
/ | \

 A E S    <---- 形成临时的 4-node
/ | | \

   E
  / \
 A   S    <---- 分裂
/ \ / \



分裂向上传导, 中间的元素需要上浮

这种树想删除节点时怎么办?

优势

2-3树在最坏情况下仍有较好的性能, 每个操作处理每个节点的时间都不会超过一个很小的常数, 且这两个操作都只会访问一条路径上的节点, 所以任何查找或者插入都不会超过对数级别

完美平衡的2-3树非常平展, 例如含有10亿个节点的2-3树的高度仅在19到30之间, 最多只需要访问30个节点就能在10亿个键中进行任意查找和插入操作

缺点

需要维护两种不同类型的节点, 查找和插入操作的实现需要大量的代码, 而且它们所产生的额外开销可能会使算法比标准二叉查找树更慢, 平衡一棵树的初衷是为消除最坏情况, 但我们希望这种保障所需的代码能够越少越好


In [ ]:

2-3-4树

跟"2-3树"类似, 允许节点保存1到3个元素

  • 2-节点: 包含 1 个元素和 2 个分支
  • 3-节点: 包含 2 个元素和 3 个分支
  • 4-节点: 包含 3 个元素和 4 个分支

2-3-4树的三种节点

2-3-4树的查找: 跟2-3树没什么本质区别

2-3-4树的插入: 同样不允许向空叶子插入, 这样就能保证所有的叶子都处在同样高度, 插入时首先尝试合并入叶子节点, 没有空间时 (向4-节点插入时) 先把叶子节点分裂, 上浮叶子节点中间的那个元素, 再递归处理上浮

2-3-4树也是只允许通过根节点分裂增加高度, 这是唯一使树的高度+1的情况

插入H, 最下层已经是4-节点, 需要将G上浮

节点超负荷时的处理

2-3树: 在3-节点中插入元素时, 会形成临时的4-节点, 然后上浮中央的元素, 称作 bottom-up 2-3

2-3-4树: 在4-节点中插入元素时, 需要先分裂成三个2-节点, 称作 top-down 2-3-4

两者的本质是一样的


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:

红黑树

据说作者 Robert Sedgewick 正遇到刚发明激光彩色打印机, 觉得打出的红色很好看, 就用了红色来区分节点, 于是叫红黑树

红黑树规则如下:

  • 每个节点不是红色就是黑色
  • 根节点总是黑色
  • 如果节点是红色, 则它的子节点必须是黑色, 反之不一定
  • 从根节点到叶节点或空子节点的每条路径, 必须包含相同数目的黑色节点, 即相同的黑色高度

红黑树的起源是 "2-3-4树", 从2-3-4树谈到Red-Black Tree, 红黑树其实是以二叉树的形式来描述2-3-4树, 这样就可以复用已经写好的二叉树代码, 然而2-3-4树节点内部可以有多个元素, 所以就用红色线把节点内部的数字连接起来, 而节点之间就用黑色线连接, 这就是红黑树的来历

同时它还是表现为二叉树的形式, 可兼容原先的查找操作

红链接 vs 红节点

讨论红黑树时, 有些人习惯使用 "红链接", 另一些习惯使用 "红节点", 两者本质是相同的

在原理上, 红链接更容易理解, 红链接意味着 "内部链接", 可以把红链接的父子两节点视为一个 "大型节点"

在实现上, 标记节点颜色比标记链接颜色更容易, 所以在代码中一般都是对节点染色, 约定 "红节点" 指 "从父节点到这个节点的链接是红色"


In [ ]:


In [ ]:


In [ ]:

左倾红黑树

1978年 Guibas 和 Sedgewick 发明最初的红黑树, 2008年 Sedgewick 对其进行了改进, 并将此命名为 LLRBT (Left-leaning red–black tree 左倾红黑树), LLRBT 相比原版红黑树要简单很多, 实现的代码量也少 LLRB.pdf

左倾红黑树通过把链接标记为红色黑色来模拟3-节点, 继承的是2-3树的思想, 但在实现中所有的节点都是标准的二叉树节点, 免去了维护两种节点的麻烦

存疑, 这个可能还是跟2-3-4树有关

  A B       2-3树
 / | \      示例一个 3-节点
/  |  \

     B      左倾红黑树, 双线为红色链接
   // \     含义: 节点A和节点B是不可分割的整体
  //   \    这里 "A+红线+B" 等于2-3树中的 "3-节点"
  A       
 / \
/   \    

     B      左倾红黑树的另一种表示
    / \     不标记链接颜色, 改为标记红色节点
   /   \    这里 "黑节点+红节点" 表示 "3-节点"
 *A*        也可以理解为由黑指向红节点的链接是红链接
 / \
/   \


左倾红黑树规则如下:

  • 红链接均为左链接 (如果既可能是左链, 又可能是右链, 需要维护的情况就增多了, 不利于理解)
  • 任何一个节点都不许有两条红链接 (否则会出现4-节点, 就变成了标准红黑树, 需要用改色消除这种情况)
  • 如果节点是左侧红链接, 则节点的左孩子不能再是左侧红链接 (早期这样表示一个4-节点, 现在已经弃用, 需要以旋转消除这种情况)
  • 该树是完美黑色平衡, 任意空链接到根节点的路径上的黑链接数量相同 (若去除颜色, 它不是算平衡树, 若把红色链接铺平, 黑色链接是完美平衡的)

示意左倾红黑树的黑色平衡, 把红链接压平可以看出与2-3节点对应

在看资料前, 一定要分清作者在讨论红黑树还是左倾红黑树, 算法第四版的红黑树实际上是左倾红黑树

改色

左倾红黑树有一个改色操作, 目的是为了消除红色节点在右侧的情况, 改色在原版红黑树中并不存在

意味着当看到 "改色", 说明这必然是左倾红黑树?

插入元素

类比2-3-4树的插入会更容易理解, 首先讨论简单的情况, 向2-节点中插入节点:

向2-节点中插入节点, 红黑树的做法:

情况 1, 向 D 插入 C:

    D
   //
   C                <- C自然放在左侧, 且C要标记为红色 (这里假设 D 是黑色)

情况 2, 向 C 插入 D:

  C            D           
  \\    =>    //
   D          C     <- 违反了 "红链接均为左链接", 需要做一次左旋修复


向2-节点中插入节点, 相应的2-3-4树中的做法:

 C        C D
/ \  =>  / | \   <- 无论哪种情况, 都形成了一个 3-节点


然后讨论复杂的情况, 向3-节点中插入节点:

向3-节点中插入节点, 红黑树的做法:

情况 3, 插入 A (对 H 右旋, 形成临时的双红链接)

  H          H 
 //         //              C 
 C     =>   C      =>     // \\ 
           //             A   H
           A

情况 4, 插入 C (对 A 左旋, 再对 H 右旋, 形成临时的双红链接)

  H           H            H 
 //          //           //          C              
 A     =>    A      =>    C     =>  // \\  
             \\          //         A   H              
              C          A 

情况 5, 插入 H (直接插入, 形成临时的双红链接)

  C              C
 //            // \\ 
 A        =>   A   H

向3-节点中插入节点, 相应的2-3-4树的做法:

上述 3 4 5 三种情况都一样, 都是形成一个临时的 4-node

 X Y         A C H
/ | \   =>  / | | \

之后, 因为左倾红黑树不允许左右孩子都为红色, 在形成临时的双红链接之后还需要改色:

对上述 3 4 5 三种情况的临时的双红链接, 需要把红链接提上去 (可能出现递归)

     /              //
    C               C
  // \\       =>   / \     
  A   H           A   H  

等价于 4-节点分裂为 2-节点

 A C H    =>    C       
/ | | \        / \      
              A   H     


总结左倾红黑树插入元素的步骤是:

可以将插入元素的步骤划分为四种基本操作

  • 插入元素 (红链接可能出现在两侧)
  • 分裂4-节点
  • 修复为左倾
  • 平衡4-节点

图示左倾红黑树插入元素四个基本操作
插入节点设为红色
如果父节点是黑色:
   如果插入节点是父节点的第一个孩子:
       插入节点在左侧, 什么都不做 (情况 1)
       插入节点在右侧, 需要旋转到左侧 (情况 2)
   如果父节点之前有一个孩子:
       之前的孩子必然是红色和左侧, 而插入节点在右侧, 完后需要改色 (情况 5)
如果父节点是红色:
   (这时祖父必然是黑色, 且父节点必然在祖父的左侧)
   如果 "祖父 -> (左) 父 -> (左) 插入节点", 对祖父右旋, 改色 (情况 3)
   如果 "祖父 -> (左) 父 -> (右) 插入节点", 对父节点左旋, 再对祖父右旋, 改色 (情况 4)

改色可能向上传递, 需要维护整条链上的颜色都符合规范



向左倾红黑树中"对应3-节点的结构"中插入元素, 对应情况 3 4 5

注: 如果这里将递归向上的最后一步改色操作, 延迟到下次插入操作之前, 这样插入完成后树中将存在4-结点, 生成2-3-4树

这两个文档有对左倾红黑树插入删除节点的较详细的描述: 左倾红黑树的C++实现理解红黑树

删除元素

总结规律是:

找到被删节点的后继节点, 以后继节点的key替换被删节点的key, 之后删除后继节点

如果删的后继节点是红的, 不会影响红黑树的性质 如果删的后继节点是黑的, 要把它变成红的

LLRBT——让理解红黑树更简单


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:

AVL 和 红黑树区别

结构和实现

  • 红黑树: 保存红黑状态, 每个节点只需要一位二进制, 也就是一个 bit
  • AVL 树: AVL 每个节点需要额外存储一个平衡值

在具体实现时, 红黑树记录颜色的这个 bit 可以塞到其他地方, 就可以不占用额外空间了, 而 AVL 树的平衡值可以用两个 bit 来存储, 然后塞到指针里

指针少了两个 bit 有什么后果?

  • 红黑树: 复杂, 插入有5种情况, 删除有6种情况, 代码量大, 编写容易出错
  • AVL 树: 相对简单

平衡度

  • 红黑树: 没有一条路径会比其他路径长两倍以上, 近似平衡
  • AVL 树: 树高差最多只有 1, 严格平衡

红黑树的树高度, 最坏 2log(n), 平均 log(n)

操作

  • 红黑树: 旋转次数较少, 但是需要染色
  • AVL 树: 旋转次数多, 只要插入或删除不满足条件就旋转

据说优化过的 AVL 和 Linux 的 RBTree 差不多, 待确认

红黑树比 AVL 树具体更高效在哪里?

  1. 如果插入一个 node 引起了树的不平衡,AVL 和 RB-Tree 都是最多只需要 2 次旋转操作,即两者都是 O(1);但是在删除 node 引起树的不平衡时,最坏情况下,AVL 需要维护从被删 node 到 root 这条路径上所有 node 的平衡性,因此需要旋转的量级 O(logN),而 RB-Tree 最多只需 3 次旋转,只需要 O(1) 的复杂度。

  2. 其次,AVL 的结构相较 RB-Tree 来说更为平衡,在插入和删除 node 更容易引起 Tree 的 unbalance,因此在大量数据需要插入或者删除时,AVL 需要 rebalance 的频率会更高。因此,RB-Tree 在需要大量插入和删除 node 的场景下,效率更高。自然,由于 AVL 高度平衡,因此 AVL 的 search 效率更高。

  3. map 的实现只是折衷了两者在 search、insert 以及 delete 下的效率。总体来说,RB-tree 的统计性能是高于 AVL 的。

为什么STL和linux都使用红黑树作为平衡树的实现?

适用场景

  • 红黑树: 适合插入删除频繁的情况
  • AVL 树: 由于旋转耗时, 适合插入删除次数比较少, 但查找多的情况

In [ ]:


In [ ]:


In [ ]:

复杂度分析

有个挺常见的表

数据结构 最坏情况 平均情况
-- 查找 / 插入 查找 / 插入
无序链表 N , N N/2 , N
有序数组 lg(N) , N lg(N) , N/2
二叉搜索树 (BST) N , N 1.39lg(N) , 1.39lg(N)
2-3树, 红黑树 2lg(N) , 2lg(N) 1.00lg(N) , 1.00lg(N)

为什么?

无序链表的复杂度解释

如果无序链表是在尾部插入的, 那么跟表中提供的数据符合

但是, 顺序查找的插入为什么是 N, 直接插入在首节点不好吗?

有序数组的复杂度解释

这个好理解, 有序数组的查找没有好坏之分, 永远是折半查找

有序数组的插入需要调整节点, 期望是要动一半的节点, 最不利时要动所有节点

二叉搜索树的复杂度解释

最坏情况其实就是退化为链表的情况, 好理解

对于平均情况的 1.39lg(N) 的解释:

首先, lg() 一般都是表示以 10 为底, 但这里应该是表示以 2 为底, 换算公式是 lg(x) = ln(x)/ln(2)

若BST完美平衡, 则查找复杂度应该是 lg(N), 但是实际平衡度是介于完美平衡和极度不平衡之间的, 这里给出的结果是 1.39lg(N), 这个 1.39lg(N) 是这么算出来的:

2 x ln(N) = 2 x ln(2) x log(N, 2) = 1.386438 x log(N, 2) = 1.39lg(N)

为什么在平均情况下, 二叉搜索树的查找/插入复杂度是 2ln(N)? 参考 http://cse.iitkgp.ac.in/~pb/algo-1-pb-10.pdf 的证明, 简要总结如下:

首先需要定义四个值...

           E                 
         /   \         
        /     \
       C       F  <---- Internal node
      / \     / \
     /   \   #   #  <---- External node 
    A     D
   / \   / \
  #   B #   #
     / \
    #   #

  • Internal path length: I(n) 所有内部节点 (正常节点) 到根节点的距离之和
  • External path length: E(n) 所有外部节点 (无数据的 dummy 节点) 到根节点的距离之和
  • Average number of comparisons for successful search: S(n) 命中查找的平均比对次数
  • Average number of comparisons for unsuccessful search: U(n) 没能命中查找的平均比对次数

之后可得:

S(n) = (I(n) + n) / n 命中查找的次数的期望 = 内部节点的平均距离 + 1 

U(n) = E(n) / (n + 1) 没有命中的查找的次数的期望, 这里为什么是 n+1 ?

E(n) = I(n) + 2n      外部节点距离和 = 内部节点距离和 + 2n, 这个可以递推出来:
                      每增加一个内部节点, 设其深度是 h, 则 dI = h, dE = 2h+2 - h, 
                      所以每增加一个内部节点, dE - dI = 2


这三个联合起来, 可以得出 S(n) 的公式, 备用

$$ S(n) = \left(1+\frac{1}{n}\right)U(n)-1 $$

然后, 考虑树的构建顺序, 设新加入的数据x被存放在树的第i个节点 (该节点记作 u[i]), 则插入这个数据时, 肯定首先执行过一次 "没有命中的查找", 次数是 U(i-1)

一旦插入了数据x, 则再次查找数据x时, 就会在节点 u[i] 这里终止, 因此, S(i) = U(i-1) + 1, 多出来的那一次查找, 是因为需要区分命中查找和没命中的查找

考虑 "依次向树上添加节点" 的整个过程, 则可以认为在总体情况下, 平均查找次数 S(n)

$$ S(n) = \frac{1}{n}\sum_{i=1}^n(U_{i-1}+1)$$

上面两个大式子联合起来是:

$$ (n+1)U(n) = 2n + U(0) + U(1) + U(2) + ... + U(n-1) $$

将 n 设为 n-1 则是:

$$ nU(n-1) = 2(n-1) + U(0) + U(1) + U(2) + ... + U(n-2) $$

相减得到 U(n) 的递推公式:

$$ U(n) = U(n-1)+\frac{2}{n+1} $$

其中 U(0) = 0, 将递推改写成通项公式是

$$ U(n) = 2\left(\frac{1}{1}+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n+1}\right) = 2H_{n+1}-2 \approx 2\ln{n} = \left(2\ln{2}\right) log _2{n}$$

H(n) 表示第n个 调和级数, 这是个发散极其缓慢的级数, 且第n个调和数与n的自然对数的差值收敛于 欧拉-马斯刻若尼常数 (约为 0.577), 参考 调和级数欧拉-马斯刻若尼常数

有了 U(n), 自然也就有了 S(n), 在n很大时, 这两个值没什么区别, 都是 2ln(N)

简要总结完了...

红黑树的复杂度解释

为什么是 2lg(N)1.00lg(N)?

爱是啥是啥吧, 先不弄了 ...

二叉查找树(BST),平衡二叉查找树(AVL),红黑树(RBT),B~/B+树(B-tree)的比较


In [ ]:


In [ ]:

跳表

现在部分场景使用跳表来替换红黑树

跳表 skiplist:

跳跃列表是按层建造的, 底层是个普通的有序链表, 每个更高层都充当下面列表的 "快速通道", 查找目标元素时从顶层列表的头元素起步, 沿着每层链表搜索, 直至找到一个大于或等于目标的元素

如果该元素大于目标元素或已到达链表末尾, 则退回当前层的上一个元素, 转入下一层进行搜索

跳表第i层中的元素按概率p (通常为1/2或1/4) 出现在第i+1层中, 选择不同p值可以在查找代价和存储代价之间获取平衡

  • 操作时间复杂度和红黑树相同
  • 代码实现更易读
  • 区间查找效率更高

缺点

  • 内存占用比红黑树大(每节点 4 指针)
  • 由于插入时是随机选择 level,cache 友好性不够好

以上这些树结构和跳表都是适合内存的数据, 如果是放在磁盘中, 插入和删除时重新平衡的开销很大, 适合磁盘的数据结构是B树系列

树堆

随机平衡二叉查找树, 树堆 Treap = Tree + Heap

Treap本身是二叉搜索树, 其左子树和右子树也分别是一个Treap, Treap记录了一个 "优先级"

优点: 插入删除简单直观, 速度也不错, 很好地平衡了编码复杂度和时间效率

缺点: 由于优先级(优先级是个堆)是随机生成的, 所以只能保证它的插入和删除操作时间复杂度大概是log(N),不能期待它的操作一定能在准确时间内完成

所以Treap常用于算法竞赛需要手写BST时, 尤其是扩展而来的Rank Tree(名次树, 查询第k大的元素, set做不了)


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:

B树

B 即 Balanced, B树是一种平衡的多路查找树, 2-3树和2-3-4树都是B树的特例

节点最大的孩子数目称为B树的阶, 所以2-3树是3阶B树, 2-3-4树是4阶B树

用阶定义的 B 树:

  • 树中每个节点最多含有 m 个孩子, m >= 2
  • 除根节点和叶子节点外, 其它每个节点至少有 ceil(m/2) 个孩子(其中 ceil() 是取上限函数)
  • 若根节点不是叶子节点, 则至少有 2 个孩子(特殊情况:没有孩子的根节点, 即整棵树只有一个根节点)
  • 所有叶子节点都出现在同一层
  • 每个非终端节点中包含有 n 个关键字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn) 其中
    • a) Ki (i=1...n) 为关键字, 且关键字按顺序升序排序 K(i-1) < Ki
    • b) Pi 为指向子树的节点, 且指针 P(i-1) 指向子树中的所有节点的关键字均小于 Ki, 但都大于 K(i-1)
    • c) 关键字的个数 n 必须满足:ceil(m/2)-1 <= n <= m-1

注意 B 树中每一个节点能包含的关键字数有一个上界和下界, 这个下界可以用一个称作 B 树的最小度数表示

一棵含有 N 个总关键字数的 m 阶的 B 树的最大高度: log_ceil(m/2) (N+1)/2+1

B树广泛用于文件系统及数据库中, 通过对每个节点存储个数的扩展, 使得对连续的数据能够进行较快的定位和访问, 能够有效减少查找时间, 提高存储的空间局部性从而减少IO操作, 实际的 B 树节点中关键字很多, 可能是从几个到几千个, 节点越多,B 树比平衡二叉树所用的磁盘 IO 操作次数将越少, 效率也越高

实际使用场景如:

  • Windows: HPFS 文件系统
  • Mac: HFS, HFS+ 文件系统
  • Linux: ResiserFS, XFS, Ext3FS, JFS 文件系统
  • 数据库: Oracle, MySQL, SQLServer 等

In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:

B+树

是应文件系统所需而产生的B树的变形树

B+树的三个特点:

  • 关键字数和子树相同
  • 非叶子节点仅用作索引, 它的关键字和子节点有重复元素
  • 叶子节点用指针连在一起

m阶的B+树和m阶的B树的异同点在于:

  1. 有n棵子树的节点中含有n-1个关键字; (有争议, 一说B+树n棵子树的节点中含有n个关键字)
  2. 所有的叶子节点中包含全部关键字的信息, 及指向含有这些关键字记录的指针, 且叶子节点本身依关键字顺序链接
  3. 所有的非终端节点可以看成是索引部分, 非终端节点不含需要查找的数据

B+树的结构比B树, 在文件系统数据库当中更有优势, 原因如下:

1. B+树的磁盘读写代价更低

B+树内部节点并没有指向关键字具体信息的指针, 其内部节点相对B树更小, 于是同一盘块能容纳的关键字数量也多, 减少了读入内存的开销

2. B+树的查询效率更加稳定

路径上的节点并不是最终指向内容的, 只是叶子节点关键字的索引, 任何关键字查找都必须走一条从根节点到叶子节点的路, 所有关键字查询的路径长度相同, 导致每个数据的查询效率相当

3. B+树更有利于数据库扫描

B树虽然提高了磁盘IO, 但没有解决元素遍历效率低下的问题, 而B+树只需要遍历叶子节点就可以解决对全部关键字信息的扫描, 对数据库中频繁使用 range query 时, B+树有更高的性能

在有数据库索引的情况下, order by 索引的值, 返回速度特别快, 因为它并没有给你真的排序, 只是遍历树而已

B树优于B+树的地方:

在成功查询时特别有利, 因为树的高度总体要比B+树矮, 有很多基于频率的搜索是选用B树, 越频繁 query 的节点越往根上走, 前提是需要对 query 做统计, 而且要对 key 做一些变化

工程实践中的特性

B树的中间节点比B+树多存了value, 同样出度的情况下, 节点更大, 相对来说CPU cache命中率不如B+树

在实际场景, 无论B树还是B+树, 树根和上面几层因为被反复query, 所以这几块基本都在内存中, 不会出现读磁盘 IO, 一般启动时就会主动换入内存

另外, B+树的扫描特性 (扫描链表串起来的叶子节点) 在无锁情况下是很难做的, 目前见到的无锁B+树叶子节点都是不串起来的


In [ ]:


In [ ]:


In [ ]:


In [ ]:

B*树

B*树是B+树的变体, 在B+树的基础上 (所有的叶子节点中包含了全部关键字的信息, 及指向含有这些关键字记录的指针), B*树中非根和非叶子节点再增加指向兄弟的指针

B*树节点已满时, 会检查兄弟节点是否满 (因为每个节点都有指向兄弟的指针), 如果兄弟节点未满则向兄弟转移关键字, 如果兄弟节点已满, 则从当前节点和兄弟节点各拿出1/3数据创建一个新节点

B+树 vs B*树的分裂

B+树的分裂: 当一个节点满时, 分配一个新的节点, 并将原节点中1/2的数据复制到新节点, 最后在父节点中增加新节点的指针, B+树的分裂只影响原节点和父节点, 不会影响兄弟节点, 它不需要指向兄弟的指针

B*树的分裂: 当一个节点满时, 如果它的下一个兄弟节点未满, 那么将一部分数据移到兄弟节点中, 再在原节点插入关键字, 最后修改父节点中兄弟节点的关键字(因为兄弟节点的关键字范围改变了), 如果兄弟也满了, 则在原节点与兄弟节点之间增加新节点, 并各复制1/3的数据到新节点, 最后在父节点增加新节点的指针

所以, B*树分配新节点的概率比B+树要低, 空间使用率更高


In [ ]:


In [ ]:


In [ ]:

R树

R树是B树的高维度扩展, 解决了在高维空间搜索等问题, 它把B树的设计思想扩展到了多维空间, 把索引替换为 Minimal Bounding Rectangle, MBR

大致是:

  • 过程节点只存索引, 只在叶子节点存数据
  • 对于二维数据, 索引是恰好能包裹住数据的最小矩形 (对于三维数据, 索引是长方体, 对于高维类推)
  • 查找时, 只查找给定数据跟索引在 MBR 上有重叠的分支
  • 插入时, 如果遇到要扩充 MBR 的情况, 就选择使 MBR 变化最小的分支

R树的查找过程

从R树的角度返回来看B树, 可以认为B树是一维的线段树, 但是完全照搬R树的实现, 会导致线段可能重叠

R树更准确的定义:

  1. 所有叶子节点包含有m至M个记录索引, 通常 m=M/2, 特殊的, 作为根节点的叶子节点的记录数可以少于m
  2. 对于所有在叶子中存储的记录, I是最小的可完全覆盖这些记录的矩形
  3. 每一个非叶子节点拥有m至M个孩子节点 (除非它是根节点)
  4. 对于在非叶子节点上的每一个条目, i是最小的可完全覆盖这些条目的矩形 (同性质2)
  5. 所有叶子节点都位于同一层, R树为平衡树

R树在现实领域中的例子: 查找20英里以内所有的餐厅, 如果没有R树, 就得把餐厅的坐标 (x,y) 分为两个字段存放在数据库中, 这样需要遍历所有餐厅获取其位置信息, 然后计算是否满足要求, 如果一个地区有100家餐厅的话, 就要进行100次位置计算操作, 应用到谷歌地图这种超大数据库中必定不可行

四叉树 vs R树

四叉树 (Quadtree) 即二叉树在划分方式上的扩展, 二叉树每次将空间减至一半, 而四叉树每次将空间划分为四个象限

R树:

  • 平衡, 没有空叶, 树高度更小
  • 在磁盘上有明显优势 (类似于在磁盘上倾向于使用B树, 而不是二叉树)
  • 插入或删除可能导致索引重组

四叉树:

  • 简单, 约束少
  • 对于高变化频率的且分布均匀的数据, 性能比R树好
  • 不平衡, 有空叶

In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:

Trie树

Trie树 (也叫字典树, 前缀树) 是自然语言处理中最常用的数据结构, 很多字符串处理任务都会用到, Trie树每个节点只包含一个字符, 从根节点到叶节点, 路径上经过的字符连接起来构成一个词

例子:

         Root
      /    |    \
     /     |     \
    清     中     华
   /  \    |      |
  华  新   华     人
 / \   |   |      |
1  大  3   4      5
   |
   学
   |
   2

Trie树可以看做普通的树对 "键的管理" 的改进: 键不是直接保存在节点中, 而是由节点在树中的位置决定, 一个节点的所有子孙都有相同的前缀, 也就是这个节点对应的字符串

类比字典的特性: 在字典里查找晃 (huang) 这个字的时候, 会发现它附近都是读音为 huang 的, 再往前翻会看到读音前缀为 huan 的字, 再往前是读音前缀为 hua 的字... 我们在查找时, 根据 abc...xyz 的顺序找到 h 前缀的部分, 再根据 ha he hu 找到 hu 前缀的部分... 最后找到 huang, 我们会发现, 越往后其读音前缀越长, 查找也越精确, 这种类似字典的树结构就是字典树

类比有限状态自动机的特性: Trie树也可以看做是个有限状态自动机 (Definite Automata, DFA), 从 Trie 树的一个节点 (状态) 转移到另一个节点 (状态) 的行为完全由状态转移函数控制, 而状态转移函数本质上是一种映射, 在Trie树的 "双数组实现" 中, 能更好的理解它为什么是个有限状态自动机

早期的 "标准 Trie 树" (例如上图) 实现结构简单, 每个节点都由指针数组存储, 每个节点的所有子节点都位于一个数组之中, 每个数组都是完全一样的, 对于英文每个数组有 27 个指针, 是典型的以 "空间换时间" 的实现

图示 abc、d、da、dda 四个字符串构成的 "标准 Trie 树", 如果是字符串, 会在节点的尾部进行标记, 没有后续字符的 branch 分支指向NULL

之后又产生了 "List Trie 树" 和 "Hash Trie 树":

  • List Trie 树: 把 "标准 Trie 树" 的定长数组改为链表, 省空间, 但是查询变成了 O(m)
  • Hash Trie 树: 把 "标准 Trie 树" 的定长数组改为键值对, Hash 本质上是一个平衡 "数组 (查询快增删慢)" 和 "可变数组 (增删快查询慢)" 优缺点的数据结构

比较复杂的实现是 "Double-array Trie 树": 小白详解 Trie 树

双数组 Trie 树和经典 Trie 树一样也是用数组实现, 但它将所有节点状态都记录到一个数组中 (Base Array)

首先需要对字符编码:

清-1,华-2,大-3,学-4,新-5,中-6,人-7

然后维护一个数组叫 base array, 其每个 "position" 对应 "节点", 每个 "position 的值" 对应 "下一个节点的转移基数"

char code:           -   -   1   2   2   2   3   6   5   7   4  
char code value:     -   -   清  华  华  华  大  中  新  人  学
base array position: 0   1   2   3   4   5   6   7   8   9   10
base array value:    1   -   3   2   2   3   6   2   3   2   6

这里需要两个函数, base(2) = 3 表示取 base array 对应位置的值, code("华") = 2 表示取事先决定好的字符编码

构造好这个结构之后, 想查找 "清华大学" 这个词在不在树里? 步骤是:

首先查找 : base(root) + code(清) = 2, 所以 在 base array 的 position 2

然后查找 : base(清) + code(华) = 5, 所以 在 base array 的 position 5

如此反复, 直到查完

注意这里有三个 , 其实对应着Trie树的三个不同节点的

                 Root
              /   |   \
             /    |    \
            清    中    华
           /  \   |     |
          华  新  华    人
         / \   |  |     |
        1  大  3  4     5
           |
           学
           |
           2

另一个查询的例子:

双数组 Trie 树的另一个数组, 是与 base array 等长的 "check array", 作用是标识出 base array 中每个状态的前一个状态, 以检验状态转移的正确性, 也可以说 check array 记忆了这条转移路径上的父节点, 如果没有 check array, 就无法检验当前节点是否与父节点处于同一链路

Trie 树是以信息换时间的数据结构, 其查询的复杂度为O(m)

怎么理解 "信息换时间", 跟交叉熵有关?


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:

总结使用场景

AVL树, 红黑树, B树, B+树, Trie树都分别应用在哪些现实场景中?

AVL 树:

最早的平衡二叉树之一, 应用相对其他数据结构比较少, Windows 对进程地址空间的管理用到了 AVL 树, 追求局部而不是非常严格整体平衡的红黑树, 当然如果场景中对插入删除不频繁, 只是对查找特别有要求, AVL 还是优于红黑

红黑树:

平衡二叉树, 工业界最主要使用, 平衡度不像 AVL 那么好, 最差情况从根到叶子的最长路是最短路的两倍, 但旋转保持平衡次数较少

  • 广泛用在 C++ 的 STL 中, 如 map 和 set 都是用红黑树实现
  • 著名的 linux 进程调度 Completely Fair Scheduler, 用红黑树管理进程控制块
  • epoll 在内核中的实现, 用红黑树管理事件块
  • nginx 中, 用红黑树管理 timer 等
  • Java 的 TreeMap 实现

B/B+树:

分支多层数少, 一般用在磁盘文件组织, 数据索引, 数据库索引

Trie树:

不平衡也不一定有序, 用在统计和排序大量字符串, 模式匹配、正则表达式, 本身是一种有限状态自动机

如果字符串长度是固定或说有限, 那Trie的深度是可控的, 需要针对实际应用来设计自己的Trie树


从工程角度区分红黑树与B+树的应用场景, 红黑树一个 node 只存一对 kv, 因此可以使用类似嵌入式链表的方式实现, 数据结构本身不管理内存, 比较轻量级, 使用更灵活也更省内存, 比如一个 node 可以同时存在若干个树或链表中, 内核中比较常见, 而B+树由于每个 node 要存多对 kv, node 结构的内存一般就要由数据结构自己来管, 是真正意义上的 container, 相对嵌入式方法实现的红黑树, 好处是用法简单, 自己管理内存更容易做 lockfree, 一个 node 存多对 kv 的方式 cpu cache 命中率更高, 所以用户态实现的高并发索引一般还是选B+树


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]: